package com.fiap.lm;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;

import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.xml.security.utils.Base64;

import org.jboss.security.SimpleGroup;
import org.jboss.security.SimplePrincipal;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

public class XMLDigestLoginModule implements LoginModule {

    private final static String OPTION_USERS = "pathXmlUsers";
    private final static String OPTION_ROLES = "pathXmlRoles";
    private final static String OPTION_HASH = "hashFunction";
    /**
     * Instância do subject corrente. 
     */
    private Subject subject;
    /**
     * Instância do CallbackHandler.
     */
    private CallbackHandler cbh;
    /**
     * Mapeamento usuário/senha.
     */
    private HashMap users;
    /**
     * Mapeamento usuário/papéis.
     */
    private HashMap roles;
    /**
     * Usuário corrente.
     */
    private String user;
    /**
     * Role do usuário corrente.
     */
    private String role;
    /**
     * Hash function 
     */
    private String hashAlgorithm;

    /**
     * Inicialização do processo de autenticação.
     */
    public void initialize(Subject subject, CallbackHandler cbh,
	    Map sharedState, Map options) {

	this.subject = subject;
	this.cbh = cbh;

	// obtendo os caminhos dos xml com as informações de usuários e papéis
	String pathXmlUsers = (String) options.get(OPTION_USERS);
	String pathXmlRoles = (String) options.get(OPTION_ROLES);

	//Obtem algoritmo de hash utilizado
	this.hashAlgorithm = (String) options.get(OPTION_HASH);

	// guarda as informações dos xmls 
	this.users = parseXML(pathXmlUsers, "usuario", "login", "senha");
	this.roles = parseXML(pathXmlRoles, "role", "login", "role-name");
    }

    /**
     * Valida usuário e senha.
     */
    public boolean login() throws LoginException {
	try {

	    NameCallback nameCallback = new NameCallback("Login");
	    PasswordCallback passwordCallback = new PasswordCallback("Senha", false);

	    // faz a requisição do login e da senha
	    Callback[] callbacks = new Callback[]{nameCallback, passwordCallback};
	    cbh.handle(callbacks);

	    // login e hash da senha digitados
	    String login = ((NameCallback) callbacks[0]).getName();
	    String senha = digest(new String(((PasswordCallback) callbacks[1]).getPassword()), this.hashAlgorithm);
	    ((PasswordCallback) callbacks[1]).clearPassword();


	    // busca o login no HashMap de usuários
	    String senhaXml = (String) users.get(login);

	    // se não encontrou usuário
	    if (senhaXml == null) {
		throw new LoginException("O usuário '" + login + "' não existe.");
	    }

	    // se a senha está incorreta
	    if (!senha.equals(senhaXml)) {
		throw new LoginException("A incorreta para o usuário '" + login + "'.");
	    }

	    // armazena as informações do usuário  
	    this.user = login;

	    // busca a role associada ao usuário
	    String roleName = (String) roles.get(login);
	    if (roleName != null) {
		// armazena a role do usuário
		this.role = roleName;
	    }

	    return true;
	} catch (IOException e) {
	    e.printStackTrace();
	} catch (UnsupportedCallbackException e) {
	    e.printStackTrace();
	}

	return false;
    }

    /**
     * Adiciona usuario e role nos Principals.
     */
    public boolean commit() throws LoginException {
	// se existe o usuário
	if (user != null) {
	    // adiciona o usuário no principals
	    subject.getPrincipals().add(new SimplePrincipal(user));

	    // adiciona a role do usuário em principals
	    if (role != null) {
		SimpleGroup sg = new SimpleGroup("Roles");
		sg.addMember(new SimplePrincipal(role));
		subject.getPrincipals().add(sg);
	    }
	}
	return true;
    }

    /**
     * Acionado em caso de problemas.
     */
    public boolean abort() throws LoginException {
	this.subject = null;
	this.cbh = null;
	this.user = null;
	return true;
    }

    /**
     * Acionado em caso de logout.
     */
    public boolean logout() throws LoginException {
	if (role != null) {
	    subject.getPrincipals().remove(role);
	}
	subject.getPrincipals().remove(user);
	return true;
    }

    /**
     * Obtém os valores contidos no xml.
     * @param pathXml
     * @param tagNo
     * @param tagChave
     * @param tagValor
     * @return
     */
    private HashMap parseXML(String pathXml, String tagNo, String tagChave, String tagValor) {
	System.out.println(pathXml);
	HashMap retorno = new HashMap();
	try {
	    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
	    DocumentBuilder db = dbf.newDocumentBuilder();
	    Document doc = db.parse(pathXml);

	    NodeList listaNos = doc.getElementsByTagName(tagNo);
	    for (int i = 0; i < listaNos.getLength(); i++) {
		Element elemento = (Element) listaNos.item(i);

		NodeList noChave = elemento.getElementsByTagName(tagChave);
		String chave = noChave.item(0).getFirstChild().getNodeValue();

		NodeList noValor = elemento.getElementsByTagName(tagValor);
		String valor = noValor.item(0).getFirstChild().getNodeValue();

		System.out.println("Chave : " + chave + " / Valor : " + valor + " !!!");
		retorno.put(chave, valor);
	    }
	} catch (FileNotFoundException e) {
	    System.out.println("O caminho do arquivo '" + pathXml + "' está incorreto.");
	} catch (ParserConfigurationException e) {
	    System.out.println("Erro ao tentar instanciar o documento xml.");
	} catch (SAXException e) {
	    System.out.println("Erro ao tentar fazer o parse do arquivo '" + pathXml + "'.");
	} catch (IOException e) {
	    System.out.println("Erro no acesso ao arquivo '" + pathXml + "'.");
	}

	return retorno;
    }

    public String digest(String senha, String hashAlgorithm) {

	try {
	    MessageDigest md = MessageDigest.getInstance(this.hashAlgorithm);
	    byte[] senhaCripto = md.digest(senha.getBytes());

	    String saida = Base64.encode(senhaCripto);

	    System.out.println("[" + saida + "]");

	    return saida;
	} catch (NoSuchAlgorithmException e) {
	    try {
		return digest(senha, "MD5");
	    } catch (Exception ex) {
		throw new RuntimeException("Erro ao calcular hash da senha");
	    }
	}
    }
}
